Validators & Data Transformation
This page covers three things — writing custom validators, transforming data before it gets saved with beforeValidation and afterValidation, and cross-field validation.
Custom Validators
Every validator is a function that returns another function. That inner function must return a Promise. This is required because validators often need to make async calls like database lookups.
Classo provides a helper called functionToPromise to reduce the boilerplate:
const { functionToPromise } = require("@lol44lol/classo/utils/functionToPromise")
Simple Validator
Here's a validator that checks whether a value contains spaces:
function noSpaceValidator(errorMessage) {
const validate = (resolve, reject, value) => {
if (value.indexOf(" ") > -1) {
resolve({ okay: false, error: errorMessage })
} else {
resolve({ okay: true })
}
}
return functionToPromise(validate)
}
The inner function receives three arguments:
resolve— call this with{ okay: true }if valid, or{ okay: false, error: "..." }if notreject— call this if an unexpected error occursvalue— the field value being validated
Add it to a field like any built-in validator:
const { DataClass } = require("@lol44lol/classo/dataclasses/base")
const { createField, types } = require("@lol44lol/classo/databases/sqlite3")
const { functionToPromise } = require("@lol44lol/classo/utils/functionToPromise")
function noSpaceValidator(errorMessage) {
const validate = (resolve, reject, value) => {
if (value.indexOf(" ") > -1) {
resolve({ okay: false, error: errorMessage })
} else {
resolve({ okay: true })
}
}
return functionToPromise(validate)
}
class UserDataClass extends DataClass {
username = createField(types.TEXT, false, false, [
noSpaceValidator("Username cannot contain spaces")
])
getName() { return "users" }
}
Async Validator
If your validator needs to do something async — like check a database — the structure is the same. Just use .then() inside the inner function:
function isUsernameAvailable(errorMessage) {
const validate = (resolve, reject, value) => {
checkDatabase(value).then(exists => {
if (exists) {
resolve({ okay: false, error: errorMessage })
} else {
resolve({ okay: true })
}
}).catch(error => {
reject({ error })
})
}
return functionToPromise(validate)
}
Validate Only Selected Fields
Sometimes you only want to validate specific fields — for example when updating a single field. Use validateOnlyPayload and pass only the fields you want to check:
const user = new UserDataClass()
user.init({ username: "john doe", password: "short" })
// only validates the password field
const result = await user.validateOnlyPayload({ password: "short" })
console.log(result)
This is useful for update operations where you don't want to re-validate unchanged fields.
beforeValidation
beforeValidation runs as soon as init() is called, before any validators run. Use it to transform the incoming value — for example trimming whitespace or normalizing a string.
It must be an async function. It receives the value and must return the transformed value:
async function trimWhitespace(value) {
return value.trim()
}
class UserDataClass extends DataClass {
username = createField(types.TEXT, false, false, [
is_required("Username is required")
], trimWhitespace) // beforeValidation is the 5th parameter
getName() { return "users" }
}
After init() runs, form_data will contain the trimmed value — the validators then run against the transformed value.
afterValidation
afterValidation runs when you call transformValidateDataToBeSaved(). Use it to transform data before saving — the most common use case is hashing a password.
It must be an async function. It receives the validated value and returns the transformed value:
const bcrypt = require("bcrypt")
async function hashPassword(value) {
return await bcrypt.hash(value, 10)
}
class UserDataClass extends DataClass {
password = createField(types.TEXT, false, false, [
is_required("Password is required"),
minLength(8, "Password must be at least 8 characters")
], null, hashPassword) // afterValidation is the 6th parameter
getName() { return "users" }
}
To get the transformed data, call transformValidateDataToBeSaved with the original payload after validation passes:
const user = new UserDataClass()
const payload = { password: "mysecretpassword" }
user.init(payload)
const validationResult = await user.validate()
if (validationResult.data.okay) {
// password is now hashed
const dataToSave = await user.transformValidateDataToBeSaved(payload)
await db.createObject(UserDataClass, dataToSave)
}
afterValidation does not change form_data — it returns a new object with the transformed values.
Cross-Field Validation
Sometimes you need to validate two fields against each other — for example making sure the username and password are not the same. Override validationAllData on your DataClass:
class UserDataClass extends DataClass {
username = createField(types.TEXT, false, false, [
is_required("Username is required")
])
password = createField(types.TEXT, false, false, [
is_required("Password is required"),
minLength(8, "Password must be at least 8 characters")
])
async validationAllData() {
const payload = this['form_data']
if (payload['username'] === payload['password']) {
return { okay: false, error: "Username and password cannot be the same" }
}
return { okay: true }
}
getName() { return "users" }
}
validationAllData runs automatically after all field validators pass when you call validate(). You can access the full payload via this['form_data']. Return { okay: true } to pass or { okay: false, error: "..." } to fail.
What's Next
- Database CRUD — save validated data to a database
- Relations — define relations between DataClasses